/**
 * DSM - Distributed Search Manager
 * Developed by Milspec Research International Pty Ltd
 * $Author: skahl $
 * $Revision: 1.9 $
 * $Date: 2007/09/28 00:23:37 $
 * (c)Copyright 2004
 * education.au limited
 * DEST
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * Neither the names education.au limited, DEST nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 * To the extent permitted by law, the copyright owners of this software and
 * its contributors:
 * (i) exclude all warranties in relation to the software; and
 * (ii) exclude liability for all losses, costs, expenses and damages arising
 * in any way from the use of the software whether arising from or in
 * relation to breach of contract, negligence or any other tort, in equity
 * or otherwise. If the software is in breach of a warranty which is
 * implied by law, the copyright owners and contributors liability is
 * limited to the replacement of the software.
 *
 * @author	gsingh
 */

package au.edu.educationau.opensource.dsm.worker;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Locale;

import au.edu.educationau.opensource.dsm.obj.SearchResult;

import com.ibm.icu.text.Collator;

/**
 * Sorts results by: Relevance - Descending order Title - Alphabetical order
 * Source - Alphabetical order Priority Interleaved - The results are sorted by
 * priority in descending relevance.
 */
public final class SearchResultSorter {

	/** Sort the results by their relevance values in a descending fashion */
	public static final String SORTED_BY_RELEVANCE = "relevance";
	/**
	 * Sort the results by their relevance values in a descending fashion,
	 * dropping items without link, description or title to the end.
	 */
	public static final String SORTED_BY_RELEVANCE_DROP = "relevance-drop";
	/** Sort the results by their display titles in an alphabetical order */
	public static final String SORTED_BY_TITLE = "title";
	/**
	 * Sort the results by their display titles in an alphabetical order,
	 * ignoring case
	 */
	public static final String SORTED_BY_TITLE_IGNORE_CASE = "title-ic";
	/**
	 * Sort the results by their display titles according to the collation rules
	 * defined by the UCA (Unicode Collation Algorithm) for the Australian
	 * locale, using canonical decomposition.
	 */
	public static final String SORTED_BY_TITLE_AUST = "title-aust";
	/** Sort the results by their sources */
	public static final String SORTED_BY_SOURCE = "source";
	/** Sort the results by their sources */
	public static final String SORTED_BY_COLLECTION_NAME = "collection";
	/**
	 * Sort the results using priority interleaving where the priority of the
	 * source is used to interleave the descending relevance.
	 */
	public static final String SORTED_BY_PRIORITY_INTERLEAVED = "priority";
	/**
	 * Sort the results using priority interleaving, additionally dropping items
	 * with negative priority and items without link, description or title to
	 * the end.
	 */
	public static final String SORTED_BY_PRIORITY_INTERLEAVED_DROP = "priority-drop";
	/** Sort the results using the identifier */
	public static final String SORTED_BY_IDENTIFIER = "identifier";
	/** Sort the results using date */
	public static final String SORTED_BY_DATE = "date";

	/**
	 * SortType prefix indicating a metadata field sort is to be done. The value
	 * following this prefix is the name of the metadata field to be sorted on.
	 */
	public static final String PREFIX_METADATA_SORT = "md:";

	/**
	 * SortType suffix indicating an inverted sort is to be done. The value
	 * preceding this suffix indicates the type of sort that is to be inverted.
	 * Eg. this can be used to turn an ascedning sort into a descending sort.
	 * NOTE: Currently only title-ic and metadata sorts can be inverted.
	 */
	public static final String SUFFIX_INVERTED_SORT = ":inv";

	/**
	 * sortBy - sorts the supplied list using the sortType directive
	 * 
	 * @param list -
	 *            the ArrayList of SearchResults
	 * @param sortType -
	 *            the sort Type directive as provided in the constants list
	 *            above
	 * @param start -
	 *            the start index of the result set
	 * @param pagesize -
	 *            the size of a page of results that will be displayed
	 * @return An Array of SearchResult objects
	 * @exception Exception
	 */
	public static SearchResult[] sortBy(ArrayList list, String sortType, int start, int pagesize) throws Exception {
		if (null != list) {
			SearchResult[] resultArray = new SearchResult[list.size()];
			resultArray = (SearchResult[]) list.toArray(resultArray);

			// is this an inverted sort?
			boolean invert = sortType.endsWith(SUFFIX_INVERTED_SORT);
			if (invert) {
				sortType = sortType.substring(0, sortType.length() - SUFFIX_INVERTED_SORT.length());
			}

			if (sortType.equals(SORTED_BY_RELEVANCE)) {
				Arrays.sort(resultArray, new RelevanceComparator());
			} else if (sortType.equals(SORTED_BY_RELEVANCE_DROP)) {
				Arrays.sort(resultArray, new RelevanceDropComparator());
			} else if (sortType.equals(SORTED_BY_TITLE)) {
				Arrays.sort(resultArray, new TitleComparator());
			} else if (sortType.equals(SORTED_BY_TITLE_IGNORE_CASE)) {
				Comparator comparator = new TitleIgnoreCaseComparator();
				if (invert)
					comparator = new InvertingComparator(comparator);
				Arrays.sort(resultArray, comparator);
			} else if (sortType.equals(SORTED_BY_TITLE_AUST)) {
				Comparator comparator = new TitleUCAComparator(new Locale("EN", "au"), true);
				if (invert)
					comparator = new InvertingComparator(comparator);
				Arrays.sort(resultArray, comparator);
			} else if (sortType.equals(SORTED_BY_SOURCE)) {
				Arrays.sort(resultArray, new RelevanceComparator());
				Arrays.sort(resultArray, new SourceComparator());
			} else if (sortType.equals(SORTED_BY_COLLECTION_NAME)) {
				Arrays.sort(resultArray, new RelevanceComparator());
				Arrays.sort(resultArray, new CollectionNameComparator());
			} else if (sortType.equals(SORTED_BY_PRIORITY_INTERLEAVED)) {
				// Priority sorting is handled within the SearchResultSet class
			} else if (sortType.equals(SORTED_BY_PRIORITY_INTERLEAVED_DROP)) {
				// Priority Drop sorting is handled within the SearchResultSet
				// class
			} else if (sortType.equals(SORTED_BY_IDENTIFIER)) {
				Arrays.sort(resultArray, new IdentifierComparator());
			} else if (sortType.equals(SORTED_BY_DATE)) {
				Arrays.sort(resultArray, new RelevanceComparator());
				Arrays.sort(resultArray, new DateComparator());
			} else if (sortType.startsWith(PREFIX_METADATA_SORT)) {
				String field = sortType.substring(PREFIX_METADATA_SORT.length());
				Comparator comparator = new ResultMetadataComparator(field, true, true, true);
				if (invert)
					comparator = new InvertingComparator(comparator);
				Arrays.sort(resultArray, comparator);
			} else {
				// sort by relevance is the backup default, in case client sends
				// a non zero
				// length sort algorithm.
				sortType = SORTED_BY_RELEVANCE;
				Arrays.sort(resultArray, new RelevanceComparator());
			}

			if (start <= resultArray.length && pagesize <= resultArray.length) {
				int actualPageQty = pagesize;
				if (start + pagesize > resultArray.length) {
					actualPageQty = resultArray.length - (start - 1);
				}
				if (actualPageQty > 0) {
					SearchResult[] shortenedArray = new SearchResult[actualPageQty];
					System.arraycopy(resultArray, start - 1, shortenedArray, 0, actualPageQty);
					return shortenedArray;
				}
			}
			return resultArray;
		} else {
			return new SearchResult[0];
		}
	}

	/** Class name display */
	public final static String classStr = "o.m.d.w.SearchResultSorter";

} // class SearchResultSorter

/**
 * Relevance Comparator - compares the relevance of each result against the
 * next. Used with java.util.Arrays sort method
 * 
 * @author gsingh
 */
class RelevanceComparator implements Comparator {

	/**
	 * @param o1
	 * @param o2
	 */
	public int compare(Object o1, Object o2) {
		if (null != o1 && null != o2 && o1 instanceof SearchResult && o2 instanceof SearchResult) {
			float relevanceDiff = ((SearchResult) o2).getRelevance() - ((SearchResult) o1).getRelevance();
			if (relevanceDiff > 0f) {
				return 1;
			} else if (relevanceDiff < 0f) {
				return -1;
			} else {
				return 0;
			}
		} else {
			return -1;
		}
	} // compare

} // class RelevanceComparator

/**
 * RelevanceDrop Comparator - compares the relevance of each result against the
 * next, dropping items with no link, description or title to the bottom.
 * 
 * The resultant ordering is (where each group is sub-ordered by relevance): -
 * items with link, description and title - items with link and description -
 * items with link and title - items with link - items with description and
 * title - items with description - items with title - remaining items
 * 
 * Used with java.util.Arrays sort method
 */
class RelevanceDropComparator implements Comparator {
	public int compare(Object o1, Object o2) {
		if (null != o1 && null != o2 && o1 instanceof SearchResult && o2 instanceof SearchResult) {
			SearchResult s1 = (SearchResult) o1;
			SearchResult s2 = (SearchResult) o2;

			int weight1 = 0;
			if (s1.getLink() != null && s1.getLink().length() > 0)
				weight1 = weight1 + 4;
			if (s1.getDescription() != null && s1.getDescription().length() > 0)
				weight1 = weight1 + 2;
			if (s1.getTitle() != null && s1.getTitle().length() > 0)
				weight1 = weight1 + 1;

			int weight2 = 0;
			if (s2.getLink() != null && s2.getLink().length() > 0)
				weight2 = weight2 + 4;
			if (s2.getDescription() != null && s2.getDescription().length() > 0)
				weight2 = weight2 + 2;
			if (s2.getTitle() != null && s2.getTitle().length() > 0)
				weight2 = weight2 + 1;

			if (weight1 != weight2) {
				return (weight1 < weight2) ? 1 : -1;
			}

			float relevanceDiff = s2.getRelevance() - s1.getRelevance();
			if (relevanceDiff > 0f) {
				return 1;
			} else if (relevanceDiff < 0f) {
				return -1;
			} else {
				return 0;
			}
		} else {
			return -1;
		}
	} // compare

} // class RelevanceComparator

/**
 * Title Comparator - compares the title of each result against the next. Used
 * with java.util.Arrays sort method
 * 
 * @author gsingh
 */
class TitleComparator implements Comparator {

	/**
	 * @param o1
	 * @param o2
	 */
	public int compare(Object o1, Object o2) {
		if (null != o1 && null != o2 && o1 instanceof SearchResult && o2 instanceof SearchResult) {
			return ((SearchResult) o1).getTitle().compareTo(((SearchResult) o2).getTitle());
		} else {
			return -1;
		}
	} // compare

} // class TitleComparator

/**
 * Title Ignore Case Comparator - compares the title of each result against the
 * next, ignoring case. Used with java.util.Arrays sort method
 */
class TitleIgnoreCaseComparator implements Comparator {
	public int compare(Object o1, Object o2) {
		if (null != o1 && null != o2 && o1 instanceof SearchResult && o2 instanceof SearchResult) {
			return ((SearchResult) o1).getTitle().compareToIgnoreCase(((SearchResult) o2).getTitle());
		} else {
			return -1;
		}
	} // compare
} // class TitleIgnoreCaseComparator

/**
 * Source Comparator - compares the source of each result against the next. Used
 * with java.util.Arrays sort method
 * 
 * @author gsingh
 */
class SourceComparator implements Comparator {

	/**
	 * Compares sources for sorting alphabetically
	 * 
	 * @param o1
	 *            is the first search result
	 * @param o2
	 *            is the second search result
	 * 
	 * @return integer between -1 and 1
	 */
	public int compare(Object o1, Object o2) {
		if (null != o1 && null != o2 && o1 instanceof SearchResult && o2 instanceof SearchResult) {
			return ((SearchResult) o1).getSource().compareTo(((SearchResult) o2).getSource());
		} else {
			return -1;
		}
	} // compare

} // class SourceComparator

/**
 * Collection Name Comparator - compares the collection name of each result
 * against the next. (as opposed to the src name, or code) Used with
 * java.util.Arrays sort method
 * 
 * @author gsingh
 */
class CollectionNameComparator implements Comparator {

	/**
	 * Compares colelction names for sorting alphabetically
	 * 
	 * @param o1
	 *            is the first search result
	 * @param o2
	 *            is the second search result
	 * 
	 * @return integer between -1 and 1
	 */
	public int compare(Object o1, Object o2) {
		if (null != o1 && null != o2 && o1 instanceof SearchResult && o2 instanceof SearchResult) {
			return ((SearchResult) o1).getCollectionName().compareToIgnoreCase(((SearchResult) o2).getCollectionName());
		} else {
			return -1;
		}
	} // compare

} // class CollectionNameComparator

/**
 * Identifier Comparator - compares the link/identifier of each result against
 * the next. Used with java.util.Arrays sort method
 * 
 * @author gsingh
 */
class IdentifierComparator implements Comparator {

	/**
	 * @param o1
	 * @param o2
	 */
	public int compare(Object o1, Object o2) {
		if (null != o1 && null != o2 && o1 instanceof SearchResult && o2 instanceof SearchResult) {
			return ((SearchResult) o1).getLink().compareTo(((SearchResult) o2).getLink());
		} else {
			return -1;
		}
	} // compare

} // class IdentifierComparator

/**
 * Date Comparator - compares the date of each result against the next. Used
 * with java.util.Arrays sort method
 * 
 * @author gsingh Date is being sorted in reverse chronological order (nulls are
 *         at the end)
 */
class DateComparator implements Comparator {

	/**
	 * @param o1
	 * @param o2
	 */
	public int compare(Object o1, Object o2) {
		if (null != o1 && null != o2 && o1 instanceof SearchResult && o2 instanceof SearchResult) {
			return ((SearchResult) o2).getDate().compareTo(((SearchResult) o1).getDate());
		} else {
			return -1;
		}
	} // compare

} // class DateComparator

/**
 * Result Metadata Comparator - compares a nominated field of result metadata on
 * each item. Options for the sort can be set through the constructor. Used with
 * java.util.Arrays sort method
 */
class ResultMetadataComparator implements Comparator {
	private String metadataFieldName = null;
	private boolean ignoreCase = false;
	private boolean nullsGoLast = false;
	private boolean blanksAreNull = false;

	/**
	 * Sets up the comparator.
	 * 
	 * @param metadataFieldName
	 *            the field name to sort on.
	 * @param ignoreCase
	 *            whether to ignore case.
	 * @param nullsGoLast
	 *            if true, results that do not contain the requested metadata
	 *            are sorted after those that do.
	 * @param blanksAreNull
	 *            if true, blank values in the metadata are considered to be
	 *            nulls for the purpose of the comparisons.
	 */
	public ResultMetadataComparator(String metadataFieldName, boolean ignoreCase, boolean nullsGoLast, boolean blanksAreNull) {
		this.metadataFieldName = metadataFieldName;
		this.ignoreCase = ignoreCase;
		this.nullsGoLast = nullsGoLast;
		this.blanksAreNull = blanksAreNull;
	} // constructor

	public int compare(Object o1, Object o2) {
		if (null != o1 && null != o2 && o1 instanceof SearchResult && o2 instanceof SearchResult) {
			String val1 = ((SearchResult) o1).getResultMetadataValue(metadataFieldName);
			String val2 = ((SearchResult) o2).getResultMetadataValue(metadataFieldName);
			// deal with blanks
			if (blanksAreNull) {
				if (val1 != null && val1.trim().length() == 0) {
					val1 = null;
				}
				if (val2 != null && val2.trim().length() == 0) {
					val2 = null;
				}
			}
			// do the comparison
			if (val1 == null) {
				if (val2 == null) {
					return 0;
				} else {
					return (nullsGoLast) ? 1 : -1;
				}
			} else if (val2 == null) {
				return (nullsGoLast) ? -1 : 1;
			} else if (ignoreCase) {
				return val1.compareToIgnoreCase(val2);
			} else {
				return val1.compareTo(val2);
			}
		} else {
			return -1;
		}
	} // compare
} // class ResultMetadataComparator

/**
 * Title UCA Comparator - compares the title of each result against the next,
 * according to the collation rules defined by the <a
 * href="http://www.unicode.org/unicode/reports/tr10/">UCA (Unicode Collation
 * Algorithm)</a> for a particular locale. Optionally uses <a
 * href="http://www.unicode.org/unicode/reports/tr15/#Decomposition">Canonical
 * Decomposition</a>.
 * 
 * Used with java.util.Arrays sort method
 */
class TitleUCAComparator implements Comparator {
	private Collator collator;

	/**
	 * Sets up the comparator.
	 * 
	 * @param locale
	 *            Locale to sort for
	 * @param useCanonicalDecomposition
	 *            characters that are canonical variants according to the
	 *            Unicode standard will be decomposed for collation.
	 */
	public TitleUCAComparator(Locale locale, boolean useCanonicalDecomposition) {
		collator = Collator.getInstance(locale);
		if (useCanonicalDecomposition) {
			collator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
		} else {
			collator.setDecomposition(Collator.NO_DECOMPOSITION);
		}
	} // constructor

	public int compare(Object o1, Object o2) {
		if (o1 instanceof SearchResult && o2 instanceof SearchResult) {
			return collator.compare(((SearchResult) o1).getTitle(), ((SearchResult) o2).getTitle());
		} else {
			return -1;
		}
	} // compare
} // class TitleUCAComparator

/**
 * Inverting Comparator - inverts the result of the nested comparator. Eg. turns
 * a sort ascending into a sort descending. Used with java.util.Arrays sort
 * method
 */
class InvertingComparator implements Comparator {
	private Comparator comparator = null;

	/**
	 * Sets up the comparator.
	 * 
	 * @param comparator
	 *            the comparator whose results are to be inverted.
	 */
	public InvertingComparator(Comparator comparator) {
		this.comparator = comparator;
	} // constructor

	public int compare(Object o1, Object o2) {
		return comparator.compare(o1, o2) * -1;
	} // compare
} // class InvertingComparator

// -- EOF
